﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using log4net;

namespace Entangle
{
    class Message
    {
        private int? userID;
        private string payload;
        private string type;
        private string signature;

        private static Random random = new Random();
        private static readonly ILog log = LogManager.GetLogger(typeof(Message));

        public Message(string pPayload, string pType)
        {
            log.DebugFormat("Creating message with payload:'{0}', type:'{1}', current userID and no signature.'", pPayload, pType);
            type = pType;
            Payload = pPayload;
            userID = Configuration.UserID;
            signature = String.Empty;
        }
        private Message(string pPayload, string pType, int? pUserID, string pSignature)
        {
            log.DebugFormat("Creating message with payload:'{0}', type:'{1}', userID:'{2}', signature:'{3}'", pPayload, pType, pUserID, pSignature);
            type = pType;
            payload = pPayload;
            userID = pUserID;
            signature = pSignature;
        }

        public DateTime ContentCreationDate
        {
            get
            {
                try
                {
                    XmlDocument xmld = new XmlDocument();
                    XmlNode xml = xmld.CreateElement("tempPayload");
                    xml.InnerXml = payload;
                    XmlNode creationTime = xml["CreationTime"];
                    if (creationTime == null)
                        throw new MessageException("Message malformed: missing Payload.CreationTime node.");

                    return DateTime.Parse(creationTime.InnerText, System.Globalization.CultureInfo.CreateSpecificCulture("pl-PL"));
                }
                catch (Exception e)
                {
                    log.Error("Error while obtaining payload of message.", e);
                    throw;
                }
            }
        }
        public string Payload
        {
            get
            {
                try
                {
                    XmlDocument xmld = new XmlDocument();
                    XmlNode xml = xmld.CreateElement("tempPayload");
                    xml.InnerXml = payload;
                    XmlNode content = xml["Content"];
                    if (content == null)
                        throw new MessageException("Message malformed: missing Payload.Content node.");

                    return content.InnerText;
                }
                catch (Exception e)
                {
                    log.Error("Error while obtaining payload of message.", e);
                    return null;
                }
            }
            set
            {
                XmlDocument xml = new XmlDocument();

                XmlElement tempPayload = xml.CreateElement("tmpPayload");

                XmlElement aContent = xml.CreateElement("Content");
                aContent.InnerText = value;
                tempPayload.AppendChild(aContent);

                XmlElement time = xml.CreateElement("CreationTime");
                time.InnerText = DateTime.Now.ToString(System.Globalization.CultureInfo.CreateSpecificCulture("pl-PL"));
                tempPayload.AppendChild(time);

                XmlElement cPad = xml.CreateElement("CryptoPadding");
                int minBytesToCPad, maxBytesToCPad;
                switch (MessageType.PayloadEncryptionType(type))
                {
                    case MessageType.EncryptionType.RSA:
                        minBytesToCPad = 0;
                        // Modulus in bytes minus 11 for padding minus currently used.
                        maxBytesToCPad = (3072 / 8 - 11) - new UTF8Encoding().GetByteCount(xml.InnerXml + cPad.OuterXml);
                        if (maxBytesToCPad < 0)
                            throw new MessageException("Content too long for RSA encryption.");
                        break;
                    case MessageType.EncryptionType.AES:
                        minBytesToCPad = 820;
                        maxBytesToCPad = 2048;
                        break;
                    case MessageType.EncryptionType.None:
                        minBytesToCPad = 0;
                        maxBytesToCPad = 0;
                        break;
                    default:
                        throw new System.Security.Cryptography.CryptographicException("Unknown encryption type!");
                }
                if (maxBytesToCPad > 0)
                {
                    byte[] padding = new byte[random.Next(minBytesToCPad, maxBytesToCPad)];
                    random.NextBytes(padding);
                    cPad.InnerText = Convert.ToBase64String(padding);
                    tempPayload.AppendChild(cPad);
                }
                payload = tempPayload.InnerXml;
            }
        }

        public string ToXMLString()
        {
            XmlDocument xml = new XmlDocument();
            XmlDeclaration dec = xml.CreateXmlDeclaration("1.0", null, null);
            xml.AppendChild(dec);
            XmlElement message = xml.CreateElement("Message");

            XmlElement user = xml.CreateElement("UserID");
            user.InnerText = userID.ToString();
            message.AppendChild(user);

            XmlElement messageType = xml.CreateElement("Type");
            messageType.InnerText = type;
            message.AppendChild(messageType);

            Cryptography crypt = Cryptography.GetInstance();

            if (MessageType.RequiresSignature(type))
            {
                XmlElement signature = xml.CreateElement("Signature");
                signature.InnerText = crypt.SignData(payload);
                signature.SetAttribute("hashAlgorithm", "SHA512");
                message.AppendChild(signature);
            }

            XmlElement pload = xml.CreateElement("Payload");
            switch (MessageType.PayloadEncryptionType(type))
            {
                case MessageType.EncryptionType.AES:
                    pload.InnerXml = payload;
                    break;
                case MessageType.EncryptionType.RSA:
                    pload.InnerText = crypt.RSAEncryptWithPartnerKey(payload);
                    break;
                case MessageType.EncryptionType.None:
                    pload.InnerText = Payload;
                    break;
            }
            message.AppendChild(pload);

            switch (MessageType.PayloadEncryptionType(type))
            {
                case MessageType.EncryptionType.AES:
                    XmlElement aesKey = xml.CreateElement("AESKey");
                    aesKey.InnerText = crypt.AESCurrentKey;
                    message.AppendChild(aesKey);
                    break;
                case MessageType.EncryptionType.RSA:
                    XmlElement rsaHash = xml.CreateElement("UsedRSAHash");
                    rsaHash.InnerText = Convert.ToBase64String(crypt.PartnerPublicKeyHash);
                    message.AppendChild(rsaHash);
                    break;
            }
            xml.AppendChild(message);

            if(MessageType.PayloadEncryptionType(type) == MessageType.EncryptionType.AES)
                    crypt.EncryptXmlElement(xml, "Payload", crypt.AESCurrentKey);

            return xml.OuterXml;
        }
        public string Type { get { return type; } }
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("Message from user: {0}, created at: {1}", userID, ContentCreationDate.ToString(System.Globalization.CultureInfo.CreateSpecificCulture("pl-PL")));
            sb.AppendLine();
            sb.AppendFormat("Message payload (actual):{0}", Payload);

            return sb.ToString();
        }
        public static Message FromXML(string sXml)
        {
            XmlDocument xmld = new XmlDocument();
            try
            {
                xmld.LoadXml(sXml);
            }
            catch (XmlException xe)
            {
                throw new MessageException("XML Invalid.", xe);
            }
            XmlElement xml = xmld.DocumentElement;

            XmlNode xnType = xml["Type"];
            if(xnType == null)
                throw new MessageException("Missing required 'Type' field.");
            string type = xnType.InnerText;

            Cryptography crypt = Cryptography.Instance;
            XmlNode xnPayload; string payload = String.Empty;
            switch (MessageType.PayloadEncryptionType(type))
            {
                case MessageType.EncryptionType.AES:
                    XmlNode xnAesKey = xml["AESKey"];
                    if (xnAesKey == null)
                        throw new MessageException("Missing required 'AESKey' field.");
                    string aesKey = xnAesKey.InnerText;
                    crypt.DecryptXmlElement(xmld, aesKey);
                    xnPayload = xml["Payload"];
                    if(xnPayload == null)
                        throw new MessageException("Missing required 'Payload' field.");
                    payload = xnPayload.InnerXml;
                    break;
                case MessageType.EncryptionType.RSA:
                    XmlNode xnRSAHash = xml["UsedRSAHash"];
                    if (xnRSAHash == null)
                        throw new MessageException("Missing required 'UsedRSAHash' field.");
                    string RSAHash = xnRSAHash.InnerText;
                    if (RSAHash != Convert.ToBase64String(Cryptography.Instance.PublicKeyHash))
                        throw new MessageException("Message payload encrypted with unknow RSA key, hash=" + RSAHash);

                    xnPayload = xml["Payload"];
                    if(xnPayload == null)
                        throw new MessageException("Missing required 'Payload' field.");
                    payload = crypt.RSADecryptWithPrivateKey(xnPayload.InnerXml);
                    break;
                default:
                    throw new MessageException("Unknown payload encryption type.");
            }

            XmlNode xnUserID = xml["UserID"];
            if(xnUserID == null)
                throw new MessageException("Missing required 'UserID' field.");

            int? userID = null;
            try{
                userID = int.Parse(xnUserID.InnerText);
            }catch(Exception e){
                throw new MessageException("UserID is not numeric!",e);
            }

            string signature = String.Empty;
            if (MessageType.RequiresSignature(type)) // Verify sender authenticity
            {
                XmlNode xnSignature = xml["Signature"];
                if (xnSignature == null)
                    throw new MessageException(String.Format("Missing required 'Signature' field (Message type '{0}' requires signature).", type));

                signature = xnSignature.InnerText;
                XmlAttribute xahAlgorithm = xnSignature.Attributes["hashAlgorithm"];
                if(xahAlgorithm == null)
                    throw new MessageException("Field Signature is missing required 'hashAlgorithm' attribute.");

                string hAlgorithm = xahAlgorithm.InnerText;
                if(hAlgorithm == null || hAlgorithm == String.Empty)
                    throw new MessageException("Field Signature is missing required 'hashAlgorithm' attribute.");

                if (!crypt.VerifyData(payload, signature, hAlgorithm))
                    throw new IdentityVerificationException("Signature didn't match partner public key");
            }

            Message m = new Message(payload, type, userID, signature);
            return m;            
        }
        public static Message FromServerString(string servStr)
        {
            string type;
            string payload = String.Empty;
            try
            {
                string[] servAr = servStr.Split(':');
                type = servAr[0];
                if (servAr.Length == 2)
                    payload = servAr[1];
                Message r = new Message(payload, type, null, String.Empty);
                r.Payload = payload;
                return r;
            }
            catch (Exception e)
            {
                log.Error("Error parsing server string!", e);
                return null;
            }
        }
        public static Message CreateErrorMessage(string type, string payload)
        {
            Message r = new Message(payload, type, null, String.Empty);
            r.Payload = payload;
            return r;
        }
    }
}
